package cn.felord.security.autoconfigure;


import cn.felord.security.autoconfigure.handler.SimpleAuthenticationEntryPoint;
import cn.felord.security.autoconfigure.jwt.BearerTokenResponse;
import cn.felord.security.autoconfigure.jwt.JwtTokenStorage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
import java.util.Set;

/**
 * jwt 认证拦截器 用于拦截 请求 提取jwt 认证
 *
 * @author dax
 * @since 2019 /11/7 23:02
 */
@Slf4j
public class BearerAccessTokenAuthenticationFilter extends OncePerRequestFilter {
    private static final String AUTHENTICATION_PREFIX = "Bearer ";
    private final AuthenticationEntryPoint authenticationEntryPoint = new SimpleAuthenticationEntryPoint();
    private final JwtDecoder jwtDecoder;
    private final JwtTokenStorage jwtTokenStorage;


    /**
     * Instantiates a new Bearer token authentication filter.
     *
     * @param jwtDecoder      the jwt decoder
     * @param jwtTokenStorage the jwt token storage
     */
    public BearerAccessTokenAuthenticationFilter(JwtDecoder jwtDecoder, JwtTokenStorage jwtTokenStorage) {
        this.jwtDecoder = jwtDecoder;
        this.jwtTokenStorage = jwtTokenStorage;
    }


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (response.isCommitted()) {
            log.debug("Response has already been committed");
            return;
        }
        // 如果已经通过认证
        if (SecurityContextHolder.getContext().getAuthentication() != null) {
            chain.doFilter(request, response);
            return;
        }
        // 获取 header 解析出 jwt 并进行认证 无token 直接进入下一个过滤器  因为  SecurityContext 的缘故 如果无权限并不会放行
        String header = request.getHeader(HttpHeaders.AUTHORIZATION);
        if (StringUtils.hasText(header) && header.startsWith(AUTHENTICATION_PREFIX)) {
            String jwtToken = header.replace(AUTHENTICATION_PREFIX, "");
            try {
                if (StringUtils.hasText(jwtToken)) {
                    this.authenticationTokenHandle(jwtToken, request);
                } else {
                    // 带安全头 没有带token
                    throw new AuthenticationCredentialsNotFoundException("Bearer Token is not found");
                }
            } catch (AuthenticationException e) {
                this.authenticationEntryPoint.commence(request, response, e);
                return;
            }
        }
        chain.doFilter(request, response);
    }

    /**
     * 具体的Token认证
     *
     * @param jwtToken jwt token
     * @param request  request
     */
    private void authenticationTokenHandle(String jwtToken, HttpServletRequest request) throws AuthenticationException {

        //有效token才会被解析出来
        Jwt jwt;
        try {
            jwt = jwtDecoder.decode(jwtToken);
        } catch (JwtException e) {
            if (log.isDebugEnabled()) {
                log.debug("token : {}  cannot be decoded", jwtToken);
            }
            throw new BadCredentialsException("invalid token");
        }

        String aud = jwt.getAudience().get(0);
       BearerTokenResponse cachedToken = jwtTokenStorage.get(aud);
        if (Objects.isNull(cachedToken)) {
            if (log.isDebugEnabled()) {
                log.debug("token : {}  is  not in cache", jwtToken);
            }
            // 缓存中不存在就算 失败了
            throw new CredentialsExpiredException("token is not in cache");
        }

        BearerTokenResponse.AccessToken accessToken = cachedToken.getAccessToken();

        String cachedAccessTokenValue = accessToken.getTokenValue();

        if (Objects.equals(jwtToken, cachedAccessTokenValue)) {
            Set<String> scopes = accessToken.getScopes();
            String userId = jwt.getClaimAsString("uid");
            this.holdAuthentication(userId, aud, scopes.toArray(new String[0]), request);
        } else {
            // token 不匹配
            if (log.isDebugEnabled()) {
                log.debug("token : {}  is  not in matched", jwtToken);
            }
            throw new BadCredentialsException("token is not matched");
        }

    }

    /**
     * hold the Authentication
     *
     * @param username username
     * @param scopes   scopes
     * @param request  request
     */
    private void holdAuthentication(String userId, String username, String[] scopes, HttpServletRequest request) {
        List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(scopes);
        SecureUser user = new SecureUser(userId, username, "[PROTECTED]", authorities);
        // 构建用户认证token
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(user, null, authorities);
        usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
        // 放入安全上下文中
        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
    }

}
